/*!
 * # Semantic UI 2.4.1 - Search
 * http://github.com/semantic-org/semantic-ui/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

;(function ($, window, document, undefined) {

    'use strict';

    window = (typeof window != 'undefined' && window.Math == Math)
        ? window
        : (typeof self != 'undefined' && self.Math == Math)
            ? self
            : Function('return this')()
    ;

    $.fn.search = function (parameters) {
        var
            $allModules = $(this),
            moduleSelector = $allModules.selector || '',

            time = new Date().getTime(),
            performance = [],

            query = arguments[0],
            methodInvoked = (typeof query == 'string'),
            queryArguments = [].slice.call(arguments, 1),
            returnedValue
        ;
        $(this)
            .each(function () {
                var
                    settings = ($.isPlainObject(parameters))
                        ? $.extend(true, {}, $.fn.search.settings, parameters)
                        : $.extend({}, $.fn.search.settings),

                    className = settings.className,
                    metadata = settings.metadata,
                    regExp = settings.regExp,
                    fields = settings.fields,
                    selector = settings.selector,
                    error = settings.error,
                    namespace = settings.namespace,

                    eventNamespace = '.' + namespace,
                    moduleNamespace = namespace + '-module',

                    $module = $(this),
                    $prompt = $module.find(selector.prompt),
                    $searchButton = $module.find(selector.searchButton),
                    $results = $module.find(selector.results),
                    $result = $module.find(selector.result),
                    $category = $module.find(selector.category),

                    element = this,
                    instance = $module.data(moduleNamespace),

                    disabledBubbled = false,
                    resultsDismissed = false,

                    module
                ;

                module = {

                    initialize: function () {
                        module.verbose('Initializing module');
                        module.get.settings();
                        module.determine.searchFields();
                        module.bind.events();
                        module.set.type();
                        module.create.results();
                        module.instantiate();
                    },
                    instantiate: function () {
                        module.verbose('Storing instance of module', module);
                        instance = module;
                        $module
                            .data(moduleNamespace, module)
                        ;
                    },
                    destroy: function () {
                        module.verbose('Destroying instance');
                        $module
                            .off(eventNamespace)
                            .removeData(moduleNamespace)
                        ;
                    },

                    refresh: function () {
                        module.debug('Refreshing selector cache');
                        $prompt = $module.find(selector.prompt);
                        $searchButton = $module.find(selector.searchButton);
                        $category = $module.find(selector.category);
                        $results = $module.find(selector.results);
                        $result = $module.find(selector.result);
                    },

                    refreshResults: function () {
                        $results = $module.find(selector.results);
                        $result = $module.find(selector.result);
                    },

                    bind: {
                        events: function () {
                            module.verbose('Binding events to search');
                            if (settings.automatic) {
                                $module
                                    .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input)
                                ;
                                $prompt
                                    .attr('autocomplete', 'off')
                                ;
                            }
                            $module
                            // prompt
                                .on('focus' + eventNamespace, selector.prompt, module.event.focus)
                                .on('blur' + eventNamespace, selector.prompt, module.event.blur)
                                .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard)
                                // search button
                                .on('click' + eventNamespace, selector.searchButton, module.query)
                                // results
                                .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown)
                                .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup)
                                .on('click' + eventNamespace, selector.result, module.event.result.click)
                            ;
                        }
                    },

                    determine: {
                        searchFields: function () {
                            // this makes sure $.extend does not add specified search fields to default fields
                            // this is the only setting which should not extend defaults
                            if (parameters && parameters.searchFields !== undefined) {
                                settings.searchFields = parameters.searchFields;
                            }
                        }
                    },

                    event: {
                        input: function () {
                            if (settings.searchDelay) {
                                clearTimeout(module.timer);
                                module.timer = setTimeout(function () {
                                    if (module.is.focused()) {
                                        module.query();
                                    }
                                }, settings.searchDelay);
                            }
                            else {
                                module.query();
                            }
                        },
                        focus: function () {
                            module.set.focus();
                            if (settings.searchOnFocus && module.has.minimumCharacters()) {
                                module.query(function () {
                                    if (module.can.show()) {
                                        module.showResults();
                                    }
                                });
                            }
                        },
                        blur: function (event) {
                            var
                                pageLostFocus = (document.activeElement === this),
                                callback = function () {
                                    module.cancel.query();
                                    module.remove.focus();
                                    module.timer = setTimeout(module.hideResults, settings.hideDelay);
                                }
                            ;
                            if (pageLostFocus) {
                                return;
                            }
                            resultsDismissed = false;
                            if (module.resultsClicked) {
                                module.debug('Determining if user action caused search to close');
                                $module
                                    .one('click.close' + eventNamespace, selector.results, function (event) {
                                        if (module.is.inMessage(event) || disabledBubbled) {
                                            $prompt.focus();
                                            return;
                                        }
                                        disabledBubbled = false;
                                        if (!module.is.animating() && !module.is.hidden()) {
                                            callback();
                                        }
                                    })
                                ;
                            }
                            else {
                                module.debug('Input blurred without user action, closing results');
                                callback();
                            }
                        },
                        result: {
                            mousedown: function () {
                                module.resultsClicked = true;
                            },
                            mouseup: function () {
                                module.resultsClicked = false;
                            },
                            click: function (event) {
                                module.debug('Search result selected');
                                var
                                    $result = $(this),
                                    $title = $result.find(selector.title).eq(0),
                                    $link = $result.is('a[href]')
                                        ? $result
                                        : $result.find('a[href]').eq(0),
                                    href = $link.attr('href') || false,
                                    target = $link.attr('target') || false,
                                    title = $title.html(),
                                    // title is used for result lookup
                                    value = ($title.length > 0)
                                        ? $title.text()
                                        : false,
                                    results = module.get.results(),
                                    result = $result.data(metadata.result) || module.get.result(value, results),
                                    returnedValue
                                ;
                                if ($.isFunction(settings.onSelect)) {
                                    if (settings.onSelect.call(element, result, results) === false) {
                                        module.debug('Custom onSelect callback cancelled default select action');
                                        disabledBubbled = true;
                                        return;
                                    }
                                }
                                module.hideResults();
                                if (value) {
                                    module.set.value(value);
                                }
                                if (href) {
                                    module.verbose('Opening search link found in result', $link);
                                    if (target == '_blank' || event.ctrlKey) {
                                        window.open(href);
                                    }
                                    else {
                                        window.location.href = (href);
                                    }
                                }
                            }
                        }
                    },
                    handleKeyboard: function (event) {
                        var
                            // force selector refresh
                            $result = $module.find(selector.result),
                            $category = $module.find(selector.category),
                            $activeResult = $result.filter('.' + className.active),
                            currentIndex = $result.index($activeResult),
                            resultSize = $result.length,
                            hasActiveResult = $activeResult.length > 0,

                            keyCode = event.which,
                            keys = {
                                backspace: 8,
                                enter: 13,
                                escape: 27,
                                upArrow: 38,
                                downArrow: 40
                            },
                            newIndex
                        ;
                        // search shortcuts
                        if (keyCode == keys.escape) {
                            module.verbose('Escape key pressed, blurring search field');
                            module.hideResults();
                            resultsDismissed = true;
                        }
                        if (module.is.visible()) {
                            if (keyCode == keys.enter) {
                                module.verbose('Enter key pressed, selecting active result');
                                if ($result.filter('.' + className.active).length > 0) {
                                    module.event.result.click.call($result.filter('.' + className.active), event);
                                    event.preventDefault();
                                    return false;
                                }
                            }
                            else if (keyCode == keys.upArrow && hasActiveResult) {
                                module.verbose('Up key pressed, changing active result');
                                newIndex = (currentIndex - 1 < 0)
                                    ? currentIndex
                                    : currentIndex - 1
                                ;
                                $category
                                    .removeClass(className.active)
                                ;
                                $result
                                    .removeClass(className.active)
                                    .eq(newIndex)
                                    .addClass(className.active)
                                    .closest($category)
                                    .addClass(className.active)
                                ;
                                event.preventDefault();
                            }
                            else if (keyCode == keys.downArrow) {
                                module.verbose('Down key pressed, changing active result');
                                newIndex = (currentIndex + 1 >= resultSize)
                                    ? currentIndex
                                    : currentIndex + 1
                                ;
                                $category
                                    .removeClass(className.active)
                                ;
                                $result
                                    .removeClass(className.active)
                                    .eq(newIndex)
                                    .addClass(className.active)
                                    .closest($category)
                                    .addClass(className.active)
                                ;
                                event.preventDefault();
                            }
                        }
                        else {
                            // query shortcuts
                            if (keyCode == keys.enter) {
                                module.verbose('Enter key pressed, executing query');
                                module.query();
                                module.set.buttonPressed();
                                $prompt.one('keyup', module.remove.buttonFocus);
                            }
                        }
                    },

                    setup: {
                        api: function (searchTerm, callback) {
                            var
                                apiSettings = {
                                    debug: settings.debug,
                                    on: false,
                                    cache: settings.cache,
                                    action: 'search',
                                    urlData: {
                                        query: searchTerm
                                    },
                                    onSuccess: function (response) {
                                        module.parse.response.call(element, response, searchTerm);
                                        callback();
                                    },
                                    onFailure: function () {
                                        module.displayMessage(error.serverError);
                                        callback();
                                    },
                                    onAbort: function (response) {
                                    },
                                    onError: module.error
                                },
                                searchHTML
                            ;
                            $.extend(true, apiSettings, settings.apiSettings);
                            module.verbose('Setting up API request', apiSettings);
                            $module.api(apiSettings);
                        }
                    },

                    can: {
                        useAPI: function () {
                            return $.fn.api !== undefined;
                        },
                        show: function () {
                            return module.is.focused() && !module.is.visible() && !module.is.empty();
                        },
                        transition: function () {
                            return settings.transition && $.fn.transition !== undefined && $module.transition('is supported');
                        }
                    },

                    is: {
                        animating: function () {
                            return $results.hasClass(className.animating);
                        },
                        hidden: function () {
                            return $results.hasClass(className.hidden);
                        },
                        inMessage: function (event) {
                            if (!event.target) {
                                return;
                            }
                            var
                                $target = $(event.target),
                                isInDOM = $.contains(document.documentElement, event.target)
                            ;
                            return (isInDOM && $target.closest(selector.message).length > 0);
                        },
                        empty: function () {
                            return ($results.html() === '');
                        },
                        visible: function () {
                            return ($results.filter(':visible').length > 0);
                        },
                        focused: function () {
                            return ($prompt.filter(':focus').length > 0);
                        }
                    },

                    get: {
                        settings: function () {
                            if ($.isPlainObject(parameters) && parameters.searchFullText) {
                                settings.fullTextSearch = parameters.searchFullText;
                                module.error(settings.error.oldSearchSyntax, element);
                            }
                        },
                        inputEvent: function () {
                            var
                                prompt = $prompt[0],
                                inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
                                    ? 'input'
                                    : (prompt !== undefined && prompt.onpropertychange !== undefined)
                                        ? 'propertychange'
                                        : 'keyup'
                            ;
                            return inputEvent;
                        },
                        value: function () {
                            return $prompt.val();
                        },
                        results: function () {
                            var
                                results = $module.data(metadata.results)
                            ;
                            return results;
                        },
                        result: function (value, results) {
                            var
                                lookupFields = ['title', 'id'],
                                result = false
                            ;
                            value = (value !== undefined)
                                ? value
                                : module.get.value()
                            ;
                            results = (results !== undefined)
                                ? results
                                : module.get.results()
                            ;
                            if (settings.type === 'category') {
                                module.debug('Finding result that matches', value);
                                $.each(results, function (index, category) {
                                    if ($.isArray(category.results)) {
                                        result = module.search.object(value, category.results, lookupFields)[0];
                                        // don't continue searching if a result is found
                                        if (result) {
                                            return false;
                                        }
                                    }
                                });
                            }
                            else {
                                module.debug('Finding result in results object', value);
                                result = module.search.object(value, results, lookupFields)[0];
                            }
                            return result || false;
                        },
                    },

                    select: {
                        firstResult: function () {
                            module.verbose('Selecting first result');
                            $result.first().addClass(className.active);
                        }
                    },

                    set: {
                        focus: function () {
                            $module.addClass(className.focus);
                        },
                        loading: function () {
                            $module.addClass(className.loading);
                        },
                        value: function (value) {
                            module.verbose('Setting search input value', value);
                            $prompt
                                .val(value)
                            ;
                        },
                        type: function (type) {
                            type = type || settings.type;
                            if (settings.type == 'category') {
                                $module.addClass(settings.type);
                            }
                        },
                        buttonPressed: function () {
                            $searchButton.addClass(className.pressed);
                        }
                    },

                    remove: {
                        loading: function () {
                            $module.removeClass(className.loading);
                        },
                        focus: function () {
                            $module.removeClass(className.focus);
                        },
                        buttonPressed: function () {
                            $searchButton.removeClass(className.pressed);
                        }
                    },

                    query: function (callback) {
                        callback = $.isFunction(callback)
                            ? callback
                            : function () {
                            }
                        ;
                        var
                            searchTerm = module.get.value(),
                            cache = module.read.cache(searchTerm)
                        ;
                        callback = callback || function () {
                        };
                        if (module.has.minimumCharacters()) {
                            if (cache) {
                                module.debug('Reading result from cache', searchTerm);
                                module.save.results(cache.results);
                                module.addResults(cache.html);
                                module.inject.id(cache.results);
                                callback();
                            }
                            else {
                                module.debug('Querying for', searchTerm);
                                if ($.isPlainObject(settings.source) || $.isArray(settings.source)) {
                                    module.search.local(searchTerm);
                                    callback();
                                }
                                else if (module.can.useAPI()) {
                                    module.search.remote(searchTerm, callback);
                                }
                                else {
                                    module.error(error.source);
                                    callback();
                                }
                            }
                            settings.onSearchQuery.call(element, searchTerm);
                        }
                        else {
                            module.hideResults();
                        }
                    },

                    search: {
                        local: function (searchTerm) {
                            var
                                results = module.search.object(searchTerm, settings.content),
                                searchHTML
                            ;
                            module.set.loading();
                            module.save.results(results);
                            module.debug('Returned full local search results', results);
                            if (settings.maxResults > 0) {
                                module.debug('Using specified max results', results);
                                results = results.slice(0, settings.maxResults);
                            }
                            if (settings.type == 'category') {
                                results = module.create.categoryResults(results);
                            }
                            searchHTML = module.generateResults({
                                results: results
                            });
                            module.remove.loading();
                            module.addResults(searchHTML);
                            module.inject.id(results);
                            module.write.cache(searchTerm, {
                                html: searchHTML,
                                results: results
                            });
                        },
                        remote: function (searchTerm, callback) {
                            callback = $.isFunction(callback)
                                ? callback
                                : function () {
                                }
                            ;
                            if ($module.api('is loading')) {
                                $module.api('abort');
                            }
                            module.setup.api(searchTerm, callback);
                            $module
                                .api('query')
                            ;
                        },
                        object: function (searchTerm, source, searchFields) {
                            var
                                results = [],
                                exactResults = [],
                                fuzzyResults = [],
                                searchExp = searchTerm.toString().replace(regExp.escape, '\\$&'),
                                matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'),

                                // avoid duplicates when pushing results
                                addResult = function (array, result) {
                                    var
                                        notResult = ($.inArray(result, results) == -1),
                                        notFuzzyResult = ($.inArray(result, fuzzyResults) == -1),
                                        notExactResults = ($.inArray(result, exactResults) == -1)
                                    ;
                                    if (notResult && notFuzzyResult && notExactResults) {
                                        array.push(result);
                                    }
                                }
                            ;
                            source = source || settings.source;
                            searchFields = (searchFields !== undefined)
                                ? searchFields
                                : settings.searchFields
                            ;

                            // search fields should be array to loop correctly
                            if (!$.isArray(searchFields)) {
                                searchFields = [searchFields];
                            }

                            // exit conditions if no source
                            if (source === undefined || source === false) {
                                module.error(error.source);
                                return [];
                            }
                            // iterate through search fields looking for matches
                            $.each(searchFields, function (index, field) {
                                $.each(source, function (label, content) {
                                    var
                                        fieldExists = (typeof content[field] == 'string')
                                    ;
                                    if (fieldExists) {
                                        if (content[field].search(matchRegExp) !== -1) {
                                            // content starts with value (first in results)
                                            addResult(results, content);
                                        }
                                        else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, content[field])) {
                                            // content fuzzy matches (last in results)
                                            addResult(exactResults, content);
                                        }
                                        else if (settings.fullTextSearch == true && module.fuzzySearch(searchTerm, content[field])) {
                                            // content fuzzy matches (last in results)
                                            addResult(fuzzyResults, content);
                                        }
                                    }
                                });
                            });
                            $.merge(exactResults, fuzzyResults)
                            $.merge(results, exactResults);
                            return results;
                        }
                    },
                    exactSearch: function (query, term) {
                        query = query.toLowerCase();
                        term = term.toLowerCase();
                        if (term.indexOf(query) > -1) {
                            return true;
                        }
                        return false;
                    },
                    fuzzySearch: function (query, term) {
                        var
                            termLength = term.length,
                            queryLength = query.length
                        ;
                        if (typeof query !== 'string') {
                            return false;
                        }
                        query = query.toLowerCase();
                        term = term.toLowerCase();
                        if (queryLength > termLength) {
                            return false;
                        }
                        if (queryLength === termLength) {
                            return (query === term);
                        }
                        search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
                            var
                                queryCharacter = query.charCodeAt(characterIndex)
                            ;
                            while (nextCharacterIndex < termLength) {
                                if (term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
                                    continue search;
                                }
                            }
                            return false;
                        }
                        return true;
                    },

                    parse: {
                        response: function (response, searchTerm) {
                            var
                                searchHTML = module.generateResults(response)
                            ;
                            module.verbose('Parsing server response', response);
                            if (response !== undefined) {
                                if (searchTerm !== undefined && response[fields.results] !== undefined) {
                                    module.addResults(searchHTML);
                                    module.inject.id(response[fields.results]);
                                    module.write.cache(searchTerm, {
                                        html: searchHTML,
                                        results: response[fields.results]
                                    });
                                    module.save.results(response[fields.results]);
                                }
                            }
                        }
                    },

                    cancel: {
                        query: function () {
                            if (module.can.useAPI()) {
                                $module.api('abort');
                            }
                        }
                    },

                    has: {
                        minimumCharacters: function () {
                            var
                                searchTerm = module.get.value(),
                                numCharacters = searchTerm.length
                            ;
                            return (numCharacters >= settings.minCharacters);
                        },
                        results: function () {
                            if ($results.length === 0) {
                                return false;
                            }
                            var
                                html = $results.html()
                            ;
                            return html != '';
                        }
                    },

                    clear: {
                        cache: function (value) {
                            var
                                cache = $module.data(metadata.cache)
                            ;
                            if (!value) {
                                module.debug('Clearing cache', value);
                                $module.removeData(metadata.cache);
                            }
                            else if (value && cache && cache[value]) {
                                module.debug('Removing value from cache', value);
                                delete cache[value];
                                $module.data(metadata.cache, cache);
                            }
                        }
                    },

                    read: {
                        cache: function (name) {
                            var
                                cache = $module.data(metadata.cache)
                            ;
                            if (settings.cache) {
                                module.verbose('Checking cache for generated html for query', name);
                                return (typeof cache == 'object') && (cache[name] !== undefined)
                                    ? cache[name]
                                    : false
                                    ;
                            }
                            return false;
                        }
                    },

                    create: {
                        categoryResults: function (results) {
                            var
                                categoryResults = {}
                            ;
                            $.each(results, function (index, result) {
                                if (!result.category) {
                                    return;
                                }
                                if (categoryResults[result.category] === undefined) {
                                    module.verbose('Creating new category of results', result.category);
                                    categoryResults[result.category] = {
                                        name: result.category,
                                        results: [result]
                                    }
                                }
                                else {
                                    categoryResults[result.category].results.push(result);
                                }
                            });
                            return categoryResults;
                        },
                        id: function (resultIndex, categoryIndex) {
                            var
                                resultID = (resultIndex + 1), // not zero indexed
                                categoryID = (categoryIndex + 1),
                                firstCharCode,
                                letterID,
                                id
                            ;
                            if (categoryIndex !== undefined) {
                                // start char code for "A"
                                letterID = String.fromCharCode(97 + categoryIndex);
                                id = letterID + resultID;
                                module.verbose('Creating category result id', id);
                            }
                            else {
                                id = resultID;
                                module.verbose('Creating result id', id);
                            }
                            return id;
                        },
                        results: function () {
                            if ($results.length === 0) {
                                $results = $('<div />')
                                    .addClass(className.results)
                                    .appendTo($module)
                                ;
                            }
                        }
                    },

                    inject: {
                        result: function (result, resultIndex, categoryIndex) {
                            module.verbose('Injecting result into results');
                            var
                                $selectedResult = (categoryIndex !== undefined)
                                    ? $results
                                        .children().eq(categoryIndex)
                                        .children(selector.results)
                                        .first()
                                        .children(selector.result)
                                        .eq(resultIndex)
                                    : $results
                                        .children(selector.result).eq(resultIndex)
                            ;
                            module.verbose('Injecting results metadata', $selectedResult);
                            $selectedResult
                                .data(metadata.result, result)
                            ;
                        },
                        id: function (results) {
                            module.debug('Injecting unique ids into results');
                            var
                                // since results may be object, we must use counters
                                categoryIndex = 0,
                                resultIndex = 0
                            ;
                            if (settings.type === 'category') {
                                // iterate through each category result
                                $.each(results, function (index, category) {
                                    resultIndex = 0;
                                    $.each(category.results, function (index, value) {
                                        var
                                            result = category.results[index]
                                        ;
                                        if (result.id === undefined) {
                                            result.id = module.create.id(resultIndex, categoryIndex);
                                        }
                                        module.inject.result(result, resultIndex, categoryIndex);
                                        resultIndex++;
                                    });
                                    categoryIndex++;
                                });
                            }
                            else {
                                // top level
                                $.each(results, function (index, value) {
                                    var
                                        result = results[index]
                                    ;
                                    if (result.id === undefined) {
                                        result.id = module.create.id(resultIndex);
                                    }
                                    module.inject.result(result, resultIndex);
                                    resultIndex++;
                                });
                            }
                            return results;
                        }
                    },

                    save: {
                        results: function (results) {
                            module.verbose('Saving current search results to metadata', results);
                            $module.data(metadata.results, results);
                        }
                    },

                    write: {
                        cache: function (name, value) {
                            var
                                cache = ($module.data(metadata.cache) !== undefined)
                                    ? $module.data(metadata.cache)
                                    : {}
                            ;
                            if (settings.cache) {
                                module.verbose('Writing generated html to cache', name, value);
                                cache[name] = value;
                                $module
                                    .data(metadata.cache, cache)
                                ;
                            }
                        }
                    },

                    addResults: function (html) {
                        if ($.isFunction(settings.onResultsAdd)) {
                            if (settings.onResultsAdd.call($results, html) === false) {
                                module.debug('onResultsAdd callback cancelled default action');
                                return false;
                            }
                        }
                        if (html) {
                            $results
                                .html(html)
                            ;
                            module.refreshResults();
                            if (settings.selectFirstResult) {
                                module.select.firstResult();
                            }
                            module.showResults();
                        }
                        else {
                            module.hideResults(function () {
                                $results.empty();
                            });
                        }
                    },

                    showResults: function (callback) {
                        callback = $.isFunction(callback)
                            ? callback
                            : function () {
                            }
                        ;
                        if (resultsDismissed) {
                            return;
                        }
                        if (!module.is.visible() && module.has.results()) {
                            if (module.can.transition()) {
                                module.debug('Showing results with css animations');
                                $results
                                    .transition({
                                        animation: settings.transition + ' in',
                                        debug: settings.debug,
                                        verbose: settings.verbose,
                                        duration: settings.duration,
                                        onComplete: function () {
                                            callback();
                                        },
                                        queue: true
                                    })
                                ;
                            }
                            else {
                                module.debug('Showing results with javascript');
                                $results
                                    .stop()
                                    .fadeIn(settings.duration, settings.easing)
                                ;
                            }
                            settings.onResultsOpen.call($results);
                        }
                    },
                    hideResults: function (callback) {
                        callback = $.isFunction(callback)
                            ? callback
                            : function () {
                            }
                        ;
                        if (module.is.visible()) {
                            if (module.can.transition()) {
                                module.debug('Hiding results with css animations');
                                $results
                                    .transition({
                                        animation: settings.transition + ' out',
                                        debug: settings.debug,
                                        verbose: settings.verbose,
                                        duration: settings.duration,
                                        onComplete: function () {
                                            callback();
                                        },
                                        queue: true
                                    })
                                ;
                            }
                            else {
                                module.debug('Hiding results with javascript');
                                $results
                                    .stop()
                                    .fadeOut(settings.duration, settings.easing)
                                ;
                            }
                            settings.onResultsClose.call($results);
                        }
                    },

                    generateResults: function (response) {
                        module.debug('Generating html from response', response);
                        var
                            template = settings.templates[settings.type],
                            isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])),
                            isProperArray = ($.isArray(response[fields.results]) && response[fields.results].length > 0),
                            html = ''
                        ;
                        if (isProperObject || isProperArray) {
                            if (settings.maxResults > 0) {
                                if (isProperObject) {
                                    if (settings.type == 'standard') {
                                        module.error(error.maxResults);
                                    }
                                }
                                else {
                                    response[fields.results] = response[fields.results].slice(0, settings.maxResults);
                                }
                            }
                            if ($.isFunction(template)) {
                                html = template(response, fields);
                            }
                            else {
                                module.error(error.noTemplate, false);
                            }
                        }
                        else if (settings.showNoResults) {
                            html = module.displayMessage(error.noResults, 'empty');
                        }
                        settings.onResults.call(element, response);
                        return html;
                    },

                    displayMessage: function (text, type) {
                        type = type || 'standard';
                        module.debug('Displaying message', text, type);
                        module.addResults(settings.templates.message(text, type));
                        return settings.templates.message(text, type);
                    },

                    setting: function (name, value) {
                        if ($.isPlainObject(name)) {
                            $.extend(true, settings, name);
                        }
                        else if (value !== undefined) {
                            settings[name] = value;
                        }
                        else {
                            return settings[name];
                        }
                    },
                    internal: function (name, value) {
                        if ($.isPlainObject(name)) {
                            $.extend(true, module, name);
                        }
                        else if (value !== undefined) {
                            module[name] = value;
                        }
                        else {
                            return module[name];
                        }
                    },
                    debug: function () {
                        if (!settings.silent && settings.debug) {
                            if (settings.performance) {
                                module.performance.log(arguments);
                            }
                            else {
                                module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
                                module.debug.apply(console, arguments);
                            }
                        }
                    },
                    verbose: function () {
                        if (!settings.silent && settings.verbose && settings.debug) {
                            if (settings.performance) {
                                module.performance.log(arguments);
                            }
                            else {
                                module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
                                module.verbose.apply(console, arguments);
                            }
                        }
                    },
                    error: function () {
                        if (!settings.silent) {
                            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
                            module.error.apply(console, arguments);
                        }
                    },
                    performance: {
                        log: function (message) {
                            var
                                currentTime,
                                executionTime,
                                previousTime
                            ;
                            if (settings.performance) {
                                currentTime = new Date().getTime();
                                previousTime = time || currentTime;
                                executionTime = currentTime - previousTime;
                                time = currentTime;
                                performance.push({
                                    'Name': message[0],
                                    'Arguments': [].slice.call(message, 1) || '',
                                    'Element': element,
                                    'Execution Time': executionTime
                                });
                            }
                            clearTimeout(module.performance.timer);
                            module.performance.timer = setTimeout(module.performance.display, 500);
                        },
                        display: function () {
                            var
                                title = settings.name + ':',
                                totalTime = 0
                            ;
                            time = false;
                            clearTimeout(module.performance.timer);
                            $.each(performance, function (index, data) {
                                totalTime += data['Execution Time'];
                            });
                            title += ' ' + totalTime + 'ms';
                            if (moduleSelector) {
                                title += ' \'' + moduleSelector + '\'';
                            }
                            if ($allModules.length > 1) {
                                title += ' ' + '(' + $allModules.length + ')';
                            }
                            if ((console.group !== undefined || console.table !== undefined) && performance.length > 0) {
                                console.groupCollapsed(title);
                                if (console.table) {
                                    console.table(performance);
                                }
                                else {
                                    $.each(performance, function (index, data) {
                                        console.log(data['Name'] + ': ' + data['Execution Time'] + 'ms');
                                    });
                                }
                                console.groupEnd();
                            }
                            performance = [];
                        }
                    },
                    invoke: function (query, passedArguments, context) {
                        var
                            object = instance,
                            maxDepth,
                            found,
                            response
                        ;
                        passedArguments = passedArguments || queryArguments;
                        context = element || context;
                        if (typeof query == 'string' && object !== undefined) {
                            query = query.split(/[\. ]/);
                            maxDepth = query.length - 1;
                            $.each(query, function (depth, value) {
                                var camelCaseValue = (depth != maxDepth)
                                    ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                                    : query
                                ;
                                if ($.isPlainObject(object[camelCaseValue]) && (depth != maxDepth)) {
                                    object = object[camelCaseValue];
                                }
                                else if (object[camelCaseValue] !== undefined) {
                                    found = object[camelCaseValue];
                                    return false;
                                }
                                else if ($.isPlainObject(object[value]) && (depth != maxDepth)) {
                                    object = object[value];
                                }
                                else if (object[value] !== undefined) {
                                    found = object[value];
                                    return false;
                                }
                                else {
                                    return false;
                                }
                            });
                        }
                        if ($.isFunction(found)) {
                            response = found.apply(context, passedArguments);
                        }
                        else if (found !== undefined) {
                            response = found;
                        }
                        if ($.isArray(returnedValue)) {
                            returnedValue.push(response);
                        }
                        else if (returnedValue !== undefined) {
                            returnedValue = [returnedValue, response];
                        }
                        else if (response !== undefined) {
                            returnedValue = response;
                        }
                        return found;
                    }
                };
                if (methodInvoked) {
                    if (instance === undefined) {
                        module.initialize();
                    }
                    module.invoke(query);
                }
                else {
                    if (instance !== undefined) {
                        instance.invoke('destroy');
                    }
                    module.initialize();
                }

            })
        ;

        return (returnedValue !== undefined)
            ? returnedValue
            : this
            ;
    };

    $.fn.search.settings = {

        name: 'Search',
        namespace: 'search',

        silent: false,
        debug: false,
        verbose: false,
        performance: true,

        // template to use (specified in settings.templates)
        type: 'standard',

        // minimum characters required to search
        minCharacters: 1,

        // whether to select first result after searching automatically
        selectFirstResult: false,

        // API config
        apiSettings: false,

        // object to search
        source: false,

        // Whether search should query current term on focus
        searchOnFocus: true,

        // fields to search
        searchFields: [
            'title',
            'description'
        ],

        // field to display in standard results template
        displayField: '',

        // search anywhere in value (set to 'exact' to require exact matches
        fullTextSearch: 'exact',

        // whether to add events to prompt automatically
        automatic: true,

        // delay before hiding menu after blur
        hideDelay: 0,

        // delay before searching
        searchDelay: 200,

        // maximum results returned from search
        maxResults: 7,

        // whether to store lookups in local cache
        cache: true,

        // whether no results errors should be shown
        showNoResults: true,

        // transition settings
        transition: 'scale',
        duration: 200,
        easing: 'easeOutExpo',

        // callbacks
        onSelect: false,
        onResultsAdd: false,

        onSearchQuery: function (query) {
        },
        onResults: function (response) {
        },

        onResultsOpen: function () {
        },
        onResultsClose: function () {
        },

        className: {
            animating: 'animating',
            active: 'active',
            empty: 'empty',
            focus: 'focus',
            hidden: 'hidden',
            loading: 'loading',
            results: 'results',
            pressed: 'down'
        },

        error: {
            source: 'Cannot search. No source used, and Semantic API module was not included',
            noResults: 'Your search returned no results',
            logging: 'Error in debug logging, exiting.',
            noEndpoint: 'No search endpoint was specified',
            noTemplate: 'A valid template name was not specified.',
            oldSearchSyntax: 'searchFullText setting has been renamed fullTextSearch for consistency, please adjust your settings.',
            serverError: 'There was an issue querying the server.',
            maxResults: 'Results must be an array to use maxResults setting',
            method: 'The method you called is not defined.'
        },

        metadata: {
            cache: 'cache',
            results: 'results',
            result: 'result'
        },

        regExp: {
            escape: /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
            beginsWith: '(?:\s|^)'
        },

        // maps api response attributes to internal representation
        fields: {
            categories: 'results',     // array of categories (category view)
            categoryName: 'name',        // name of category (category view)
            categoryResults: 'results',     // array of results (category view)
            description: 'description', // result description
            image: 'image',       // result image
            price: 'price',       // result price
            results: 'results',     // array of results (standard)
            title: 'title',       // result title
            url: 'url',         // result url
            action: 'action',      // "view more" object name
            actionText: 'text',        // "view more" text
            actionURL: 'url'          // "view more" url
        },

        selector: {
            prompt: '.prompt',
            searchButton: '.search.button',
            results: '.results',
            message: '.results > .message',
            category: '.category',
            result: '.result',
            title: '.title, .name'
        },

        templates: {
            escape: function (string) {
                var
                    badChars = /[&<>"'`]/g,
                    shouldEscape = /[&<>"'`]/,
                    escape = {
                        "&": "&amp;",
                        "<": "&lt;",
                        ">": "&gt;",
                        '"': "&quot;",
                        "'": "&#x27;",
                        "`": "&#x60;"
                    },
                    escapedChar = function (chr) {
                        return escape[chr];
                    }
                ;
                if (shouldEscape.test(string)) {
                    return string.replace(badChars, escapedChar);
                }
                return string;
            },
            message: function (message, type) {
                var
                    html = ''
                ;
                if (message !== undefined && type !== undefined) {
                    html += ''
                        + '<div class="message ' + type + '">'
                    ;
                    // message type
                    if (type == 'empty') {
                        html += ''
                            + '<div class="header">No Results</div class="header">'
                            + '<div class="description">' + message + '</div class="description">'
                        ;
                    }
                    else {
                        html += ' <div class="description">' + message + '</div>';
                    }
                    html += '</div>';
                }
                return html;
            },
            category: function (response, fields) {
                var
                    html = '',
                    escape = $.fn.search.settings.templates.escape
                ;
                if (response[fields.categoryResults] !== undefined) {

                    // each category
                    $.each(response[fields.categoryResults], function (index, category) {
                        if (category[fields.results] !== undefined && category.results.length > 0) {

                            html += '<div class="category">';

                            if (category[fields.categoryName] !== undefined) {
                                html += '<div class="name">' + category[fields.categoryName] + '</div>';
                            }

                            // each item inside category
                            html += '<div class="results">';
                            $.each(category.results, function (index, result) {
                                if (result[fields.url]) {
                                    html += '<a class="result" href="' + result[fields.url] + '">';
                                }
                                else {
                                    html += '<a class="result">';
                                }
                                if (result[fields.image] !== undefined) {
                                    html += ''
                                        + '<div class="image">'
                                        + ' <img src="' + result[fields.image] + '">'
                                        + '</div>'
                                    ;
                                }
                                html += '<div class="content">';
                                if (result[fields.price] !== undefined) {
                                    html += '<div class="price">' + result[fields.price] + '</div>';
                                }
                                if (result[fields.title] !== undefined) {
                                    html += '<div class="title">' + result[fields.title] + '</div>';
                                }
                                if (result[fields.description] !== undefined) {
                                    html += '<div class="description">' + result[fields.description] + '</div>';
                                }
                                html += ''
                                    + '</div>'
                                ;
                                html += '</a>';
                            });
                            html += '</div>';
                            html += ''
                                + '</div>'
                            ;
                        }
                    });
                    if (response[fields.action]) {
                        html += ''
                            + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
                            + response[fields.action][fields.actionText]
                            + '</a>';
                    }
                    return html;
                }
                return false;
            },
            standard: function (response, fields) {
                var
                    html = ''
                ;
                if (response[fields.results] !== undefined) {

                    // each result
                    $.each(response[fields.results], function (index, result) {
                        if (result[fields.url]) {
                            html += '<a class="result" href="' + result[fields.url] + '">';
                        }
                        else {
                            html += '<a class="result">';
                        }
                        if (result[fields.image] !== undefined) {
                            html += ''
                                + '<div class="image">'
                                + ' <img src="' + result[fields.image] + '">'
                                + '</div>'
                            ;
                        }
                        html += '<div class="content">';
                        if (result[fields.price] !== undefined) {
                            html += '<div class="price">' + result[fields.price] + '</div>';
                        }
                        if (result[fields.title] !== undefined) {
                            html += '<div class="title">' + result[fields.title] + '</div>';
                        }
                        if (result[fields.description] !== undefined) {
                            html += '<div class="description">' + result[fields.description] + '</div>';
                        }
                        html += ''
                            + '</div>'
                        ;
                        html += '</a>';
                    });

                    if (response[fields.action]) {
                        html += ''
                            + '<a href="' + response[fields.action][fields.actionURL] + '" class="action">'
                            + response[fields.action][fields.actionText]
                            + '</a>';
                    }
                    return html;
                }
                return false;
            }
        }
    };

})(jQuery, window, document);
